/*
 * Copyright 2000-2006 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.intellij.gwt.module.index;

import com.intellij.gwt.GwtBundle;
import com.intellij.gwt.GwtConfiguration;
import com.intellij.gwt.make.GwtCompilerPaths;
import com.intellij.gwt.module.GwtModulesManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.FileTypeManager;
import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentIterator;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.MultiValuesMap;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileAdapter;
import com.intellij.openapi.vfs.VirtualFileEvent;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.psi.*;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;

import java.io.*;
import java.util.*;

/**
 * @author nik
 */
public class GwtFilesIndexImpl extends GwtFilesIndex implements ProjectComponent {
  private static final Logger LOG = Logger.getInstance("com.intellij.gwt.module.index.GwtFilesIndexImpll");
  @NonNls private static final String META_TAG_NAME = "meta";
  @NonNls private static final String GWT_MODULE_META_NAME = "gwt:module";
  @NonNls private static final String CACHES_DIR_NAME = "caches";
  private static final byte CURRENT_VERSION = 0;
  private Project myProject;
  private FileTypeManager myFileTypeManager;
  private VirtualFileManager myVirtualFileManager;
  private PsiManager myPsiManager;
  private Set<VirtualFile> myGwtXmlFiles = new THashSet<VirtualFile>();
  private Map<String, GwtHtmlFileInfo> myUrl2Info = new THashMap<String, GwtHtmlFileInfo>();
  private final Set<VirtualFile> myDirtyHtmlFiles = new THashSet<VirtualFile>();
  private MultiValuesMap<String, String> myModuleName2HtmlFiles = new MultiValuesMap<String, String>();
  private GwtFilesIndexImpl.MyPsiTreeChangeListener myPsiTreeListener;
  private MyVirtualFileListener myVirtualFileListener;

  public GwtFilesIndexImpl(final Project project, final FileTypeManager fileTypeManager, VirtualFileManager virtualFileManager, final PsiManager psiManager) {
    myProject = project;
    myFileTypeManager = fileTypeManager;
    myVirtualFileManager = virtualFileManager;
    myPsiManager = psiManager;
  }

  public void projectOpened() {
    Runnable startup = getBuildIndicesRunnable();
    boolean registerListeners = false;
    if (ApplicationManager.getApplication().isUnitTestMode()) {
      startup.run();
      registerListeners = true;
    }
    else if (GwtConfiguration.getInstance().isValidConfiguration()) {
      StartupManager.getInstance(myProject).registerStartupActivity(startup);
      registerListeners = true;
    }

    if (registerListeners) {
      registerListeners();
    }
  }

  public void registerListeners() {
    myPsiTreeListener = new MyPsiTreeChangeListener();
    myPsiManager.addPsiTreeChangeListener(myPsiTreeListener);
    myVirtualFileListener = new MyVirtualFileListener();
    myVirtualFileManager.addVirtualFileListener(myVirtualFileListener);
  }

  public Runnable getBuildIndicesRunnable() {
    return new Runnable() {
      public void run() {
        clearMaps();
        loadCache();
        buildIndex();
      }
    };
  }

  public void projectClosed() {
    if (myPsiTreeListener != null) {
      myPsiManager.removePsiTreeChangeListener(myPsiTreeListener);
    }
    if (myVirtualFileListener != null) {
      myVirtualFileManager.removeVirtualFileListener(myVirtualFileListener);
    }
    if (!ApplicationManager.getApplication().isUnitTestMode()) {
      saveCache();
    }
    clearMaps();
  }

  private void clearMaps() {
    myGwtXmlFiles.clear();
    myUrl2Info.clear();
  }

  @NonNls
  @NotNull
  public String getComponentName() {
    return "GwtFilesIndex";
  }

  public void initComponent() {
  }

  public void disposeComponent() {
  }

  private void loadCache() {
    final File file = getCacheFile();
    if (!file.exists()) {
      return;
    }

    try {
      DataInputStream input = new DataInputStream(new BufferedInputStream(new FileInputStream(file)));
      try {
        final byte version = input.readByte();
        if (version != CURRENT_VERSION) {
          return;
        }
        int size = input.readInt();
        while (size-- > 0) {
          final String url = input.readUTF();
          final GwtHtmlFileInfo info = new GwtHtmlFileInfo(input);
          putHtmlInfo(url, info);
        }
      }
      finally {
        input.close();
      }
    }
    catch (IOException e) {
      myUrl2Info.clear();
    }
  }

  private void saveCache() {
    final File file = getCacheFile();
    FileUtil.createParentDirs(file);
    try {
      file.createNewFile();
      DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
      try {
        output.writeByte(CURRENT_VERSION);
        output.writeInt(myUrl2Info.size());
        for (Map.Entry<String, GwtHtmlFileInfo> entry : myUrl2Info.entrySet()) {
          output.writeUTF(entry.getKey());
          entry.getValue().write(output);
        }
      }
      finally {
        output.close();
      }
    }
    catch (IOException e) {
      LOG.info(e);
      FileUtil.delete(file);
    }
  }

  private File getCacheFile() {
    final String cacheFileName = myProject.getName() + "." + Integer.toHexString(FileUtil.toSystemIndependentName(myProject.getProjectFilePath()).hashCode());
    return new File(new File(GwtCompilerPaths.getOutputRoot(), CACHES_DIR_NAME), cacheFileName);
  }

  private void buildIndex() {
    final ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
    if (indicator != null) {
      indicator.pushState();
      indicator.setIndeterminate(true);
      indicator.setText(GwtBundle.message("progress.text.building.gwt.indices"));
    }
    final ProjectFileIndex index = ProjectRootManager.getInstance(myProject).getFileIndex();

    final List<VirtualFile> htmlFiles = new ArrayList<VirtualFile>();
    index.iterateContent(new ContentIterator() {
      public boolean processFile(VirtualFile fileOrDir) {
        if (!fileOrDir.isDirectory()) {
          if (isGwtXmlFile(fileOrDir)) {
            myGwtXmlFiles.add(fileOrDir);
          }

          final FileType fileType = myFileTypeManager.getFileTypeByFile(fileOrDir);
          if (fileType == StdFileTypes.HTML) {
            htmlFiles.add(fileOrDir);
          }
        }
        return true;
      }
    });

    if (indicator != null) {
      indicator.setIndeterminate(false);
    }
    final int size = htmlFiles.size();
    for (int i = 0; i < size; i++) {
      VirtualFile file = htmlFiles.get(i);
      if (indicator != null) {
        indicator.setFraction((double)i / size);
      }
      final GwtHtmlFileInfo info = myUrl2Info.get(file.getUrl());
      if (info == null || info.getTimestamp() != file.getTimeStamp()) {
        refreshInfo(file);
      }
    }

    if (indicator != null) {
      indicator.popState();
    }
  }

  private static boolean isGwtXmlFile(final VirtualFile file) {
    return file.getName().endsWith(GwtModulesManager.GWT_XML_SUFFIX);
  }

  public VirtualFile[] getGwtXmlFiles() {
    return myGwtXmlFiles.toArray(new VirtualFile[myGwtXmlFiles.size()]);
  }

  public VirtualFile[] getHtmlFilesByModule(@NotNull String moduleName) {
    refreshDirtyFiles();
    final Collection<String> urls = myModuleName2HtmlFiles.get(moduleName);
    if (urls == null) return VirtualFile.EMPTY_ARRAY;

    List<VirtualFile> files = new ArrayList<VirtualFile>(urls.size());
    for (String url : urls) {
      final VirtualFile file = myVirtualFileManager.findFileByUrl(url);
      if (file != null) {
        files.add(file);
      }
    }
    return files.toArray(new VirtualFile[files.size()]);
  }

  private void refreshDirtyFiles() {
    VirtualFile[] dirtyFiles;
    synchronized(myDirtyHtmlFiles) {
      dirtyFiles = myDirtyHtmlFiles.toArray(new VirtualFile[myDirtyHtmlFiles.size()]);
      myDirtyHtmlFiles.clear();
    }

    for (VirtualFile file : dirtyFiles) {
      refreshInfo(file);
    }
  }

  private void refreshInfo(final VirtualFile file) {
    if (!file.isValid()) {
      return;
    }

    final PsiFile psiFile = myPsiManager.findFile(file);
    if (psiFile == null) {
      return;
    }

    final GwtHtmlFileInfo info = new GwtHtmlFileInfo(file.getTimeStamp());
    psiFile.accept(new PsiRecursiveElementVisitor() {
      public void visitXmlTag(XmlTag tag) {
        if (META_TAG_NAME.equalsIgnoreCase(tag.getName())) {
          final String name = tag.getAttributeValue("name");
          if (name != null && name.equalsIgnoreCase(GWT_MODULE_META_NAME)) {
            final String content = tag.getAttributeValue("content");
            if (content != null) {
              info.addGwtModule(content);
            }
          }
        }
        super.visitXmlTag(tag);
      }
    });
    putHtmlInfo(file.getUrl(), info);
  }

  private void putHtmlInfo(final String url, final GwtHtmlFileInfo info) {
    removeHtmlInfo(url);
    myUrl2Info.put(url, info);
    final Set<String> modules = info.getGwtModules();
    if (modules != null) {
      for (String moduleName : modules) {
        myModuleName2HtmlFiles.put(moduleName, url);
      }
    }
  }

  private void removeHtmlInfo(final String url) {
    final GwtHtmlFileInfo oldEntry = myUrl2Info.remove(url);
    if (oldEntry != null) {
      final Set<String> modules = oldEntry.getGwtModules();
      if (modules != null) {
        for (String moduleName : modules) {
          myModuleName2HtmlFiles.remove(moduleName, url);
        }
      }
    }
  }

  private void handleCreateDeleteFile(final VirtualFile virtualFile, final boolean create) {
    if (virtualFile.isDirectory()) {
      final VirtualFile[] children = virtualFile.getChildren();
      for (VirtualFile child : children) {
        handleCreateDeleteFile(child, create);
      }
      return;
    }

    if (isGwtXmlFile(virtualFile)) {
      if (create) {
        myGwtXmlFiles.add(virtualFile);
      }
      else {
        myGwtXmlFiles.remove(virtualFile);
      }
    }
    else if (myFileTypeManager.getFileTypeByFile(virtualFile) == StdFileTypes.HTML) {
      if (create) {
        synchronized (myDirtyHtmlFiles) {
          myDirtyHtmlFiles.add(virtualFile);
        }
      }
      else {
        removeHtmlInfo(virtualFile.getUrl());
      }
    }
  }


  private class MyPsiTreeChangeListener extends PsiTreeChangeAdapter {
    private void handleAddRemoveFile(final PsiTreeChangeEvent event, final boolean add) {
      final PsiElement child = event.getChild();
      if (!(child instanceof XmlFile)) return;

      final XmlFile file = (XmlFile)child;
      final String name = file.getName();
      if (name == null) return;

      final VirtualFile virtualFile = file.getVirtualFile();
      if (virtualFile == null) return;

      handleCreateDeleteFile(virtualFile, add);
    }

    public void childAdded(PsiTreeChangeEvent event) {
      handleAddRemoveFile(event, true);
      processChangeInHtml(event);
    }

    public void childRemoved(PsiTreeChangeEvent event) {
      handleAddRemoveFile(event, false);
      processChangeInHtml(event);
    }


    public void childReplaced(PsiTreeChangeEvent event) {
      processChangeInHtml(event);
    }

    public void childMoved(PsiTreeChangeEvent event) {
      processChangeInHtml(event);
    }

    public void propertyChanged(PsiTreeChangeEvent event) {
      processChangeInHtml(event);
    }

    public void childrenChanged(PsiTreeChangeEvent event) {
      processChangeInHtml(event);
    }

    private void processChangeInHtml(final PsiTreeChangeEvent event) {
      final PsiElement parent = event.getParent();
      if (parent == null) return;

      final PsiFile psiFile = parent.getContainingFile();
      if (psiFile == null) return;

      final VirtualFile virtualFile = psiFile.getVirtualFile();
      if (virtualFile == null) return;

      if (myFileTypeManager.getFileTypeByFile(virtualFile) == StdFileTypes.HTML) {
        synchronized (myDirtyHtmlFiles) {
          myDirtyHtmlFiles.add(virtualFile);
        }
      }
    }
  }

  private class MyVirtualFileListener extends VirtualFileAdapter {
    public void fileCreated(VirtualFileEvent event) {
      final VirtualFile file = event.getFile();
      handleCreateDeleteFile(file, true);
    }

    public void fileDeleted(VirtualFileEvent event) {
      final VirtualFile file = event.getFile();
      handleCreateDeleteFile(file, false);
    }
  }
}
